package evl

import (
	"testing"

	"gitcode.com/deyiyangyang/bampoo/ast"
	"gitcode.com/deyiyangyang/bampoo/obj"
)

// TestSetBasic tests basic set! functionality
func TestSetBasic(t *testing.T) {
	env := obj.NewEnvironment()
	evaluator := New(env)

	// First define a variable
	define := &ast.DefineExpr{
		Name:  ast.Symbol{Value: "x"},
		Value: &ast.IntegerLiteral{Value: 10},
	}

	_, err := evaluator.Eval(define)
	if err != nil {
		t.Fatalf("unexpected error defining variable: %v", err)
	}

	// Then set it to a new value
	set := &ast.SetExpr{
		Name:  ast.Symbol{Value: "x"},
		Value: &ast.IntegerLiteral{Value: 20},
	}

	result, err := evaluator.Eval(set)
	if err != nil {
		t.Fatalf("unexpected error setting variable: %v", err)
	}

	// Should return nil
	if _, ok := result.(*obj.NilValue); !ok {
		t.Fatalf("expected NilValue, got %T", result)
	}

	// Check that the variable was updated
	val, ok := env.Get("x")
	if !ok {
		t.Fatal("expected to find x in environment")
	}

	intVal, ok := val.(*obj.IntegerValue)
	if !ok {
		t.Fatalf("expected IntegerValue, got %T", val)
	}

	if intVal.Value != 20 {
		t.Errorf("expected x to be 20, got %d", intVal.Value)
	}
}

// TestSetUndefinedVariable tests that set! fails on undefined variables
func TestSetUndefinedVariable(t *testing.T) {
	env := obj.NewEnvironment()
	evaluator := New(env)

	// Try to set an undefined variable
	set := &ast.SetExpr{
		Name:  ast.Symbol{Value: "undefined"},
		Value: &ast.IntegerLiteral{Value: 42},
	}

	_, err := evaluator.Eval(set)
	if err == nil {
		t.Fatal("expected error for setting undefined variable")
	}

	if !containsSubstring(err.Error(), "cannot set! undefined variable: undefined") {
		t.Errorf("expected error message to contain 'cannot set! undefined variable: undefined', got '%s'", err.Error())
	}
}

// TestSetStringValue tests setting a variable to a string value
func TestSetStringValue(t *testing.T) {
	env := obj.NewEnvironment()
	evaluator := New(env)

	// Define a variable with integer value
	define := &ast.DefineExpr{
		Name:  ast.Symbol{Value: "message"},
		Value: &ast.IntegerLiteral{Value: 123},
	}

	_, err := evaluator.Eval(define)
	if err != nil {
		t.Fatalf("unexpected error defining variable: %v", err)
	}

	// Set it to a string value
	set := &ast.SetExpr{
		Name:  ast.Symbol{Value: "message"},
		Value: &ast.StringLiteral{Value: "hello world"},
	}

	_, err = evaluator.Eval(set)
	if err != nil {
		t.Fatalf("unexpected error setting variable: %v", err)
	}

	// Check that the variable was updated
	val, ok := env.Get("message")
	if !ok {
		t.Fatal("expected to find message in environment")
	}

	strVal, ok := val.(*obj.StringValue)
	if !ok {
		t.Fatalf("expected StringValue, got %T", val)
	}

	if strVal.Value != "hello world" {
		t.Errorf("expected message to be 'hello world', got '%s'", strVal.Value)
	}
}

// TestSetWithExpression tests setting a variable with an expression
func TestSetWithExpression(t *testing.T) {
	env := obj.NewEnvironment()
	evaluator := New(env)

	// Define a variable
	define := &ast.DefineExpr{
		Name:  ast.Symbol{Value: "result"},
		Value: &ast.IntegerLiteral{Value: 0},
	}

	_, err := evaluator.Eval(define)
	if err != nil {
		t.Fatalf("unexpected error defining variable: %v", err)
	}

	// Set it to the result of an arithmetic expression
	set := &ast.SetExpr{
		Name: ast.Symbol{Value: "result"},
		Value: &ast.ArithmeticExpr{
			Operator: "+",
			Operands: []ast.Expression{
				&ast.IntegerLiteral{Value: 5},
				&ast.IntegerLiteral{Value: 3},
				&ast.IntegerLiteral{Value: 2},
			},
		},
	}

	_, err = evaluator.Eval(set)
	if err != nil {
		t.Fatalf("unexpected error setting variable: %v", err)
	}

	// Check that the variable was updated
	val, ok := env.Get("result")
	if !ok {
		t.Fatal("expected to find result in environment")
	}

	intVal, ok := val.(*obj.IntegerValue)
	if !ok {
		t.Fatalf("expected IntegerValue, got %T", val)
	}

	if intVal.Value != 10 { // 5 + 3 + 2 = 10
		t.Errorf("expected result to be 10, got %d", intVal.Value)
	}
}

// TestSetMultipleTimes tests setting a variable multiple times
func TestSetMultipleTimes(t *testing.T) {
	env := obj.NewEnvironment()
	evaluator := New(env)

	// Define a variable
	define := &ast.DefineExpr{
		Name:  ast.Symbol{Value: "counter"},
		Value: &ast.IntegerLiteral{Value: 0},
	}

	_, err := evaluator.Eval(define)
	if err != nil {
		t.Fatalf("unexpected error defining variable: %v", err)
	}

	// Set it multiple times
	for i := 1; i <= 5; i++ {
		set := &ast.SetExpr{
			Name:  ast.Symbol{Value: "counter"},
			Value: &ast.IntegerLiteral{Value: int64(i)},
		}

		_, err := evaluator.Eval(set)
		if err != nil {
			t.Fatalf("unexpected error setting variable (iteration %d): %v", i, err)
		}

		// Check that the variable was updated
		val, ok := env.Get("counter")
		if !ok {
			t.Fatalf("expected to find counter in environment (iteration %d)", i)
		}

		intVal, ok := val.(*obj.IntegerValue)
		if !ok {
			t.Fatalf("expected IntegerValue, got %T (iteration %d)", val, i)
		}

		if intVal.Value != int64(i) {
			t.Errorf("expected counter to be %d, got %d (iteration %d)", i, intVal.Value, i)
		}
	}
}

// TestSetWithSymbolReference tests setting a variable to another variable's value
func TestSetWithSymbolReference(t *testing.T) {
	env := obj.NewEnvironment()
	evaluator := New(env)

	// Define two variables
	define1 := &ast.DefineExpr{
		Name:  ast.Symbol{Value: "x"},
		Value: &ast.IntegerLiteral{Value: 100},
	}

	define2 := &ast.DefineExpr{
		Name:  ast.Symbol{Value: "y"},
		Value: &ast.IntegerLiteral{Value: 200},
	}

	_, err := evaluator.Eval(define1)
	if err != nil {
		t.Fatalf("unexpected error defining x: %v", err)
	}

	_, err = evaluator.Eval(define2)
	if err != nil {
		t.Fatalf("unexpected error defining y: %v", err)
	}

	// Set x to y's value
	set := &ast.SetExpr{
		Name:  ast.Symbol{Value: "x"},
		Value: &ast.Symbol{Value: "y"},
	}

	_, err = evaluator.Eval(set)
	if err != nil {
		t.Fatalf("unexpected error setting variable: %v", err)
	}

	// Check that x was updated to y's value
	val, ok := env.Get("x")
	if !ok {
		t.Fatal("expected to find x in environment")
	}

	intVal, ok := val.(*obj.IntegerValue)
	if !ok {
		t.Fatalf("expected IntegerValue, got %T", val)
	}

	if intVal.Value != 200 {
		t.Errorf("expected x to be 200, got %d", intVal.Value)
	}
}

// TestSetInBegin tests set! inside a begin block
func TestSetInBegin(t *testing.T) {
	env := obj.NewEnvironment()
	evaluator := New(env)

	// Define a variable
	define := &ast.DefineExpr{
		Name:  ast.Symbol{Value: "result"},
		Value: &ast.IntegerLiteral{Value: 0},
	}

	_, err := evaluator.Eval(define)
	if err != nil {
		t.Fatalf("unexpected error defining variable: %v", err)
	}

	// Use begin to set the variable and return its new value
	begin := &ast.BeginExpr{
		Body: []ast.Expression{
			&ast.SetExpr{
				Name:  ast.Symbol{Value: "result"},
				Value: &ast.IntegerLiteral{Value: 42},
			},
			&ast.Symbol{Value: "result"},
		},
	}

	result, err := evaluator.Eval(begin)
	if err != nil {
		t.Fatalf("unexpected error in begin block: %v", err)
	}

	// Should return the final value of result
	intVal, ok := result.(*obj.IntegerValue)
	if !ok {
		t.Fatalf("expected IntegerValue, got %T", result)
	}

	if intVal.Value != 42 {
		t.Errorf("expected result to be 42, got %d", intVal.Value)
	}

	// Check that the variable was updated
	val, ok := env.Get("result")
	if !ok {
		t.Fatal("expected to find result in environment")
	}

	intVal2, ok := val.(*obj.IntegerValue)
	if !ok {
		t.Fatalf("expected IntegerValue, got %T", val)
	}

	if intVal2.Value != 42 {
		t.Errorf("expected result to be 42 in environment, got %d", intVal2.Value)
	}
}

// TestSetErrorPropagation tests that errors in the value expression are properly propagated
func TestSetErrorPropagation(t *testing.T) {
	env := obj.NewEnvironment()
	evaluator := New(env)

	// Define a variable
	define := &ast.DefineExpr{
		Name:  ast.Symbol{Value: "x"},
		Value: &ast.IntegerLiteral{Value: 10},
	}

	_, err := evaluator.Eval(define)
	if err != nil {
		t.Fatalf("unexpected error defining variable: %v", err)
	}

	// Try to set it to an undefined symbol (should fail)
	set := &ast.SetExpr{
		Name:  ast.Symbol{Value: "x"},
		Value: &ast.Symbol{Value: "undefined-symbol"},
	}

	_, err = evaluator.Eval(set)
	if err == nil {
		t.Fatal("expected error for setting variable to undefined symbol")
	}

	if !containsSubstring(err.Error(), "undefined symbol: undefined-symbol") {
		t.Errorf("expected error message to contain 'undefined symbol: undefined-symbol', got '%s'", err.Error())
	}

	// Check that x was not changed
	val, ok := env.Get("x")
	if !ok {
		t.Fatal("expected to find x in environment")
	}

	intVal, ok := val.(*obj.IntegerValue)
	if !ok {
		t.Fatalf("expected IntegerValue, got %T", val)
	}

	if intVal.Value != 10 {
		t.Errorf("expected x to remain 10, got %d", intVal.Value)
	}
}